Skip to content

feat(operator): pod management#345

Open
bmcquilkin-sentry wants to merge 9 commits into
mainfrom
bmcquilkin/operator/pods
Open

feat(operator): pod management#345
bmcquilkin-sentry wants to merge 9 commits into
mainfrom
bmcquilkin/operator/pods

Conversation

@bmcquilkin-sentry

@bmcquilkin-sentry bmcquilkin-sentry commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Adds Pod management abilities to the operator. Instead of generating a Deployment, the operator will generate and manage Pods. Uses a ConfigMap per CR to track the Pod generations. Watches Pod events and uses a health classifier to determine how to respond to them and reconcile.

This is a different implementation than the original test we did and is based on Kopf's official suggestion for how to reconcile: https://docs.kopf.dev/en/latest/reconciliation/#level-based-triggering

For each StreamingPipeline CR, the operator starts a daemon in the same container that owns the reconciliation for that CR. Updates and events only set a flag that requests reconcile. The daemon runs a reconcile on one of these triggers or the 60s health check timer. Since we only set a flag, multiple events on the same CR will only trigger one reconcile (until it is processed). We also avoid the risk of multiple reconciles on the same CR.

@bmcquilkin-sentry
bmcquilkin-sentry requested a review from a team as a code owner July 20, 2026 21:21
)
core.delete_namespaced_config_map(
name=configmap.metadata.name, namespace=workload_namespace
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Orphaned Deployments after upgrade

High Severity

The operator no longer deletes previously created Deployment workloads, and deployment RBAC was removed. On upgrade, old Deployments keep running their Pods while the new path also creates managed Pods for the same pipeline, so consumers can run twice and double-process traffic.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 50b3b68. Configure here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is real if we test the operator with Deployments on s4s2 before merging

namespace=namespace,
field_manager=FIELD_MANAGER,
force_conflicts=True,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing workload ownership checks

Medium Severity

apply_pod and save_generations apply with force_conflicts and never verify streams.sentry.io/owner-uid. Unlike _apply for ConfigMaps, a second StreamingPipeline that renders the same base name can overwrite another pipeline’s Pods or generation ledger instead of failing permanently.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 50b3b68. Configure here.

Comment thread sentry_streams_k8s/sentry_streams_k8s/operator/operator.py Outdated
Comment thread sentry_streams_k8s/sentry_streams_k8s/operator/generations.py Outdated
@linear-code

linear-code Bot commented Jul 20, 2026

Copy link
Copy Markdown

STREAM-1609

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

There are 4 total unresolved issues (including 3 from previous reviews).

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit d2ee769. Configure here.

@bmcquilkin-sentry
bmcquilkin-sentry force-pushed the bmcquilkin/operator/pods branch from d2ee769 to 50b3b68 Compare July 20, 2026 23:06
@getsentry getsentry deleted a comment from sentry Bot Jul 20, 2026
@getsentry getsentry deleted a comment from cursor Bot Jul 20, 2026
Comment thread sentry_streams_k8s/sentry_streams_k8s/operator/reconcile.py Outdated
Comment thread sentry_streams_k8s/sentry_streams_k8s/operator/operator.py Outdated
Comment thread sentry_streams_k8s/sentry_streams_k8s/operator/operator.py
Comment thread sentry_streams_k8s/sentry_streams_k8s/operator/pod_resources.py
Comment thread sentry_streams_k8s/sentry_streams_k8s/operator/operator.py
# Waiting states caused by an invalid workload specification.
# Should raise a PermanentError and not trigger replacement:

PERMANENT_WAITING_REASONS = frozenset({"InvalidImageName"})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unhandled waiting reasons stay silent

Medium Severity

pod_health only classifies ErrImagePull, ImagePullBackOff, and InvalidImageName waiting states. Other common failures such as CreateContainerConfigError produce no reason, so Pods are neither replaced, marked permanent, nor listed under unhealthyPods, leaving pipelines stuck with readyReplicas at 0 and little signal why.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit db11345. Configure here.

desired_configmaps=set(),
logger=logger,
)
scheduler.discard(uid)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Old Deployments left orphaned

High Severity

Deployment pruning was removed while switching to direct Pod management, and neither reconcile nor cleanup deletes owner-labeled Deployments. Existing Deployment-backed workloads can keep running beside newly created Pods after upgrade or deletion, risking duplicate consumer processing.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit ca2e22e. Configure here.

@fpacifici fpacifici left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please see my comments inline for the details. Overall:

  • I don't think you need a configMap for the generaiton state. If you can do that with the status of a CR, please avoid the configMap complexity.
  • Some high level design constraints: Untyped dictionaries, breakdown into modules, public/private interfaces. Comments are inline
  • Consider breaking this down to make the task of reviewing tractable. A suggestion: One PR for the pod spec manipulation logic, one PR for the reconcile code, one PR that wire things up together in the operator. Failing to do so will make the review process really long as it requires the reviewer to allocate a lot of time to review each iteration.

Comment thread sentry_streams_k8s/sentry_streams_k8s/operator/generations.py Outdated
Comment thread sentry_streams_k8s/sentry_streams_k8s/operator/pod_resources.py Outdated
Comment thread sentry_streams_k8s/sentry_streams_k8s/operator/pod_resources.py Outdated
dyn: DynamicClient,
namespace: str,
owner_uid: str,
workload_set: str | None = None,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is a workload_set ?

def apply_pod(dyn: DynamicClient, pod: Mapping[str, Any], namespace: str) -> None:
_pod_resource(dyn).server_side_apply(
body=pod,
name=pod_metadata(pod)["name"],

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You have a pod_name method but you are not using here. Any reason?

Comment on lines +109 to +119
def pod_keep_key(pod: Mapping[str, Any], health: Mapping[str, Any]) -> tuple[bool, int, str]:
return (cast(bool, health["ready"]), pod_generation(pod), pod_name(pod))


def pod_template_from_deployment(
deployment: Mapping[str, Any],
) -> tuple[Mapping[str, Any], Mapping[str, Any]]:
template = cast(Mapping[str, Any], deployment["spec"]["template"])
return cast(Mapping[str, Any], template.get("metadata", {}) or {}), cast(
Mapping[str, Any], template["spec"]
)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A general note about this procedural design. If you feel this is the best option I would not push back as this is your coding style and it is a legit design. Though, since your system is doing a lot of manipulation of complex nested data structures, have you considered making this more object oriented by creating classes for the main concepts you are manipulating (Spec, Pod, ConfigMap, etc) and add methods to extract or transform them, plus modules with the application logic to reconcile and call the api ?
This would make a design emerge as it would:

  • force you to package the methods with the data types they manipulate
  • separate the data type manipulation from the execution of the application logic (calling api)

IF you like sticking with this style better I think you will have to do more work to properly modularize the system:

  • You will need properly structured types rather than relying on untyped dictionaries
  • You need to make the break down into modules more deliberate: examples could be having a pod module, a configmap module, maybe container or status. Each one of these contains the functions to manipulate these data structures. Then you'll need modules to perform the orchestration work: apply/delete/patch etc.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test is only testing the pod_health function.
Please add one single test to test the various accessor functions that return data from the pod or make them private (pod_metadata, pod_labels, etc)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not see unit tests for this module. Am I missing something ?

Comment on lines +93 to +104
monkeypatch.setattr(
"sentry_streams_k8s.operator.reconcile.list_owned_pods",
list_pods,
)
monkeypatch.setattr(
"sentry_streams_k8s.operator.reconcile.apply_pod",
lambda _dyn, pod, _namespace: applied.append(dict(pod)),
)
monkeypatch.setattr(
"sentry_streams_k8s.operator.reconcile.delete_pod",
lambda _dyn, name, _namespace, force=False: deleted.append((name, force)),
)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please do not monkeypatch methods you built.
This test seems like an integration test. So you should avoid mocking or monkeypatching what is not strictly needed. Please mock just the k8s API object.

Also I think this is monkeypatching the wrong path as delete_pod, apply_pod and list_owned_pods are not in the reconcile module. Unless you need to monkeypatch them there because that's where they are used.

return applied, deleted, result, ledger


def test_build_pipeline_pod_stamps_identity_without_mutating_template() -> None:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does this mean ? (I guess this is AI generated, am I right ? )

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

There are 6 total unresolved issues (including 4 from previous reviews).

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Want reviews to match your repository better? Bugbot Learning can learn team-specific rules from PR activity. A team admin can enable Learning in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit c2ce18e. Configure here.

patch.status["config_version"] = compute_config_version(consumer["pipeline_config"])
patch.status["replicas"] = {"primary": replicas - canary, "canary": canary}
patch.status["workload_namespace"] = workload_namespace
@kopf.on.event("", "v1", "pods", labels={MANAGED_BY_LABEL: FIELD_MANAGER})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Helm chart RBAC fully removed

High Severity

The Helm chart deletes rbac.yaml entirely while the operator ServiceAccount remains. The new Pod watcher and Pod/ConfigMap management need list/watch/create/patch/delete permissions, so a Helm install leaves the operator unauthorized for its core reconcile path.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit c2ce18e. Configure here.

)
if stopped:
break
await _wait_for_reconcile(event, stopped, timeout)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Daemon reconciles stale CR spec

High Severity

reconcile_pipeline_daemon keeps using the spec captured when the daemon started. Updates only call notify, and _get_pipeline_status already fetches the live CR but discards its spec. After a CR change, reconcile can keep applying the old desired state until the operator restarts.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit c2ce18e. Configure here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants